package com.mvc.servlet;

import com.mvc.annotation.Autowired;
import com.mvc.annotation.Controller;
import com.mvc.annotation.RequestMapping;
import com.mvc.annotation.Service;
import com.mvc.interceptor.Interceptor;
import com.mvc.model.MappedHandler;
import com.mvc.model.MethodParameter;
import com.mvc.model.Result;
import com.mvc.util.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Kirito
 * @description:
 * @date 2020/04/09
 */
public class DispatcherServlet extends HttpServlet {

    private final String CONTEXT_CONFIG_LOCATION = "configLocation";

    private final String CLASSPATH = "classpath";

    private final String CLASSPATH_DELIMITER = ":";


    /*spring mvc核心Servlet，负责将请求转发到对应的controller，因此在初始化时就需要将请求和controller的映射关系缓存起来*/
    private List<MappedHandler> handlerMappings;

    /*存储包扫描时获取的类的全限定名*/
    private List<String> classNames;

    /*ioc容器*/
    private Map<String, Object> ioc;

    /*默认的配置文件路径*/
    private String configLocation;

    private List<Interceptor> interceptors;

    /**
     * 在Servlet规范中定义的初始化方法，Servlet容器在加载Servlet类的时候会调用init方法，在这里可以做一些初始化工作
     *
     * @param config ServletConfig --> 可以获取到web.xml中设置的一些参数
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        // 1.解析servlet初始化参数
        prepareInit(config);

        // 2.解析xml配置文件
        parseConfig(configLocation);

        // 3.Bean初始化
        doInstance();

        // 4.依赖注入
        populateBean();

        // 5.解析handler，建立uri与method映射关系
        initHandlerMappings();
    }

    private void initHandlerMappings() {
        if (ioc == null || ioc.isEmpty()) return;
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {

            Class<?> clazz = entry.getValue().getClass();
            if (!clazz.isAnnotationPresent(Controller.class)) continue;

            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                StringBuilder sb = new StringBuilder();
                if (clazz.isAnnotationPresent(RequestMapping.class)) {
                    // 添加controller上RequestMapping中的value属性
                    sb.append(clazz.getAnnotation(RequestMapping.class).value());
                }
                if (!method.isAnnotationPresent(RequestMapping.class)) continue;
                // 添加controller方法上的url
                sb.append(method.getAnnotation(RequestMapping.class).value());

                Parameter[] parameters = method.getParameters();
                List<MethodParameter> methodParameters = new ArrayList<>(parameters.length);
                for (int i = 0; i < parameters.length; i++) {
                    MethodParameter parameter = new MethodParameter();
                    parameter.setName(parameters[i].getName());
                    parameter.setType(parameters[i].getType());
                    parameter.setIndex(i);
                    methodParameters.add(parameter);
                }
                MappedHandler handler = new MappedHandler();
                handler.setHandler(entry.getValue());
                handler.setUri(sb.toString());
                handler.setMethod(method);
                handler.setParameters(methodParameters);
                handlerMappings.add(handler);
            }
        }

    }

    private void populateBean() {
        if (ioc == null || ioc.isEmpty()) return;
        try {
            for (Map.Entry<String, Object> entry : ioc.entrySet()) {
                Class<?> clazz = entry.getValue().getClass();
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                    if (!field.isAnnotationPresent(Autowired.class)) continue;
                    Autowired autowired = field.getAnnotation(Autowired.class);
                    Object reference = null;
                    if (StringUtils.isNotEmpty(autowired.value().trim())) {
                        // 如果Autowired指定了注入的bean id，根据id从ioc获取依赖对象
                        reference = ioc.get(autowired.value());
                    } else if (ioc.get(field.getName()) != null) {
                        // 如果以字段名作为id从ioc容器中能获取到相应的对象
                        reference = ioc.get(field.getName());
                    } else {
                        // 以上情况均不满足，说明是接口注入，根据字段类型去ioc容器找对应的实现类
                        Class<?> fieldClass = field.getType();
                        reference = ioc.values().stream().filter(o ->
                                fieldClass.isAssignableFrom(o.getClass())).findFirst().orElse(null);
                    }

                    if (null == reference) continue;
                    field.set(entry.getValue(), reference);
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }


    }

    private void doInstance() {
        if (classNames == null || classNames.isEmpty()) return;
        try {
            for (String className : classNames) {
                Class<?> clazz = Class.forName(className);
                String beanId = "";
                if (clazz.isAnnotationPresent(Controller.class)) {
                    Controller controller = clazz.getAnnotation(Controller.class);
                    beanId = StringUtils.isEmpty(controller.value().trim()) ?
                            firstToLowCase(clazz.getSimpleName()) : controller.value().trim();

                } else if (clazz.isAnnotationPresent(Service.class)) {
                    Service service = clazz.getAnnotation(Service.class);
                    beanId = StringUtils.isEmpty(service.value().trim()) ?
                            firstToLowCase(clazz.getSimpleName()) : service.value().trim();
                }
                if (StringUtils.isEmpty(beanId.trim())) continue;
                ioc.put(beanId, clazz.newInstance());
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    private String firstToLowCase(String beanName) {
        if (StringUtils.isEmpty(beanName)) return "";
        char[] chars = beanName.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    private void parseConfig(String location) {
        try {
            InputStream resource = this.getClass().getClassLoader().getResourceAsStream(location);
            Document document = new SAXReader().read(resource);
            Element rootElement = document.getRootElement();
            // 处理包扫描路径
            List<Element> scanElements = rootElement.selectNodes("//scan-packaging");
            for (Element scanElement : scanElements) {
                doScan(scanElement.attributeValue("value"));
            }

            // 处理拦截器
            List<Element> interceptors = rootElement.selectNodes("//interceptor");
            for (Element element : interceptors) {
                String className = element.attributeValue("class");
                Class<?> clazz = Class.forName(className);
                Interceptor interceptor = (Interceptor) clazz.newInstance();
                this.interceptors.add(interceptor);
            }
        } catch (DocumentException | IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    private void doScan(String packageName) throws IOException {
        String path = packageName.replaceAll("\\.", "/");
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Enumeration<URL> resources = classLoader.getResources(path);
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            String filePath = url.getFile();
            File baseFile = new File(filePath);
            File[] files = baseFile.listFiles();
            if (null == files) {
                continue;
            }
            for (File file : files) {
                String fileName = file.getName();
                if (file.isDirectory()) {
                    doScan(packageName + "." + file.getName());
                } else {
                    if (file.getName().endsWith(".class")) {
                        int index = fileName.indexOf(".class");
                        fileName = fileName.substring(0, index);
                        String className = packageName + "." + fileName;
                        classNames.add(className);
                    }
                }
            }
        }
    }

    private void prepareInit(ServletConfig config) {
        String initParameter = config.getInitParameter(CONTEXT_CONFIG_LOCATION);
        this.configLocation = getConfigLocation(initParameter);
        this.classNames = new ArrayList<>();
        this.handlerMappings = new ArrayList<>();
        this.interceptors = new ArrayList<>();
        this.ioc = new ConcurrentHashMap<>();
    }

    private String getConfigLocation(String initParameter) {
        if (!initParameter.contains(CLASSPATH)) return initParameter;
        int index = initParameter.indexOf(CLASSPATH_DELIMITER);
        return initParameter.substring(index + 1);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            doDispatch(req, resp);
        } catch (Exception e) {
            Result result = new Result();
            result.setStatus("500");
            result.setMessage("Internal server error. Details: " + e.getMessage());
            resp.getWriter().print(JsonUtils.object2Json(result));
        }

    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        String uri = req.getRequestURI();
        MappedHandler handler = getMappedHandler(uri);

        Result result = new Result();
        if (null == handler) {
            result.setStatus("404");
            result.setMessage("Can not found any handler for [" + uri + "]");
            resp.getWriter().print(JsonUtils.object2Json(result));
            return;
        }

        if (!interceptors.isEmpty()) {
            for (Interceptor interceptor : interceptors) {
                boolean success = interceptor.preHandler(req, resp, handler);
                if (!success) {
                    return;
                }
            }
        }

        Map<String, String[]> parameterMap = req.getParameterMap();
        List<MethodParameter> parameters = handler.getParameters();
        Object[] args = new Object[parameters.size()];
        for (MethodParameter parameter : parameters) {
            if (parameter.getType() == HttpServletRequest.class) {
                args[parameter.getIndex()] = req;
            } else if (parameter.getType() == HttpServletResponse.class) {
                args[parameter.getIndex()] = resp;
            } else {
                String[] strings = parameterMap.get(parameter.getName());
                args[parameter.getIndex()] = strings[0];
            }
        }

        try {
            Method method = handler.getMethod();
            method.setAccessible(true);
            result = (Result) method.invoke(handler.getHandler(), args);
            resp.getWriter().print(JsonUtils.object2Json(result));
        } catch (IllegalAccessException | InvocationTargetException e) {
            result.setStatus("500");
            result.setMessage("Internal server error. Details: " + e.getMessage());
            resp.getWriter().print(JsonUtils.object2Json(result));
            e.printStackTrace();
        }
    }

    public MappedHandler getMappedHandler(String uri) {
        if (handlerMappings == null || handlerMappings.isEmpty()) return null;
        return handlerMappings.stream().filter(mappedHandler ->
                StringUtils.equals(mappedHandler.getUri(), uri)).findFirst().orElse(null);
    }
}
